from tkinter import *
from tkinter import ttk
import random


# 绘制坐标图 
def init_canvas():

    # 绘制坐标轴(350*350)
    canvas.create_rectangle(m, m, w, h)
    for i in range(m, w, m):
        # X轴坐标
        canvas.create_text(i, w+20, text=str(i - m),anchor="s")
        # Y轴坐标
        canvas.create_text(30, h - i, text=str(i))
# 设置数据
def set_data():
    rx = w - m
    ry = h - m 
    for i in range(size):
        x = random.random() * rx
        y = random.random() * ry
        # 这里的默认截距是10，斜率是0.5即y=0.5x+10
        c = 0 if y < x * 0.5 + 10 else 1
        # 将数据添加到列表中
        data.append([x, y, c])
# 绘制随机坐标
def show_data():
    for point in data:
        # 返回坐标点数组
        x, y, c = point
        # print(point)
        # 如果等于1显示红色圆圈否则显示黑色矩形
        if c == 1:
            canvas.create_oval(x + m - 2, h - y - 2, x + m + 2, h - y + 2, fill='red')
        else:
            canvas.create_rectangle(x + m - 2, h - y -2 , x + m + 2, h - y + 2, fill="black")
        
# 线性分布
def show_line():
    global data
    new_data = data.copy()
    # 先排序
    new_data.sort(key=lambda point: point[0])
    # 对X轴进行分组，50个坐标点为一组
    tmp = []
    up = []
    down = []
    max_x = m
    for d in new_data:
        x = int(d[0])
        if x > max_x:
            # 下一个分组边界
            max_x += m
            # 对分组排序
            up.sort(key=lambda point: point[1])
            down.sort(key=lambda point: point[1])
            # 取每组的前两个点
            tmp += up[:2] + down[:2]
            # 清空分组
            up = []
            down = []
        else:
            # 组内选各取一个点
            if d[2] == 1:
                # 只保留红色点
                up.append(d)
            else:
                # 只保留黑色点
               down.append(d)
    # 修改原数组
    data = tmp
    show()

# 初始化数据与坐标
def show():
    canvas.delete("all")
    init_canvas()
    show_data()
    show_label.config(text=f"斜率：{slope:.2f}, 截距：{bias:.2f}")

# 选择列表事件
def ml(event):
    xz = algorithm_var.get()
    # 选择算法
    if xz == "最小二乘法":
        sum_zx()
    elif xz == "梯度下降法":
        sum_xj()

# 显示函数
def  linear(): 
    for x in range(w-m):
        y = x * slope + bias
        new_y = h - y
        canvas.create_rectangle(x+m, new_y,  x+m+1,new_y+1)
    # 显示截距和斜率
    show_label.config(text=f"斜率：{slope:.2f}, 截距：{bias:.2f}")


# 最小二乘法
def sum_zx():
    global slope, bias
    # 初始化
    sum_x = sum_y = sum_xy = sum_xx = count = 0
    for x, y, _ in data:
        sum_x += x
        sum_y += y
        sum_xy += x * y
        sum_xx += x * x
        count += 1
    dit_y = count * sum_xy - sum_x * sum_y
    dit_x = count * sum_xx - sum_x * sum_x
    # 计算斜率
    slope = dit_y / dit_x
    y1 = sum_y / count
    y0 = (slope * sum_x) / count
    # 计算截距
    bias = y1 - y0
    linear()

# 梯度下降法
def sum_xj():
    global slope, bias
    len_data = len(data)
    # 步长
    step = 0.000001
    # 开始迭代
    for j in range(max):
        # 初始化截距和斜率的权重
        err = slope_w = bias_w = 0
        for x, y, c in data:
            # 计算误差
            err = y - (slope * x + bias)
            # 这里求其中一个变量的斜率（偏导数）： -2 * 误差 * X，-2 * 误差
            slope_w += -2 * err * x
            bias_w += -2 * err
        # 梯度下降是递减，反之梯度上升就是递加
        # 减小斜率误差
        slope -= (slope_w / len_data) * step
        # 减少截距误差
        bias -= (bias_w / len_data) * step
    linear()



root = Tk()
root.title("线性回归（最小二乘法）")

# 数据
w = 400
h = 400
m = 50
data = []
# 斜率
slope = 1
# 截距
bias = 0
# 随机坐标点数
size = 100
# 最大迭代次数
max = 1000

algorithm_var = StringVar()
algorithm_var.set("请选择算法")

frame = Frame(root)
frame.pack()
Label(frame, text="线性回归").pack(side=LEFT)

combobox = ttk.Combobox(frame, textvariable=algorithm_var,values=["最小二乘法", "梯度下降法"], state="readonly")
combobox.pack(side=LEFT)
# 绑定 <<ComboboxSelected>> 事件
combobox.bind("<<ComboboxSelected>>", ml)
Button(frame, text="线性分布", command=show_line).pack(side=LEFT)

canvas = Canvas(root, width= w + 21, height= h + 21)
canvas.pack()

show_label = Label(root, text="斜率：0, 截距：0")
show_label.pack()


set_data()
show()

root.mainloop()